using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace LabyrinthProject
{
    class Camera
    {
        #region fields

        private Player player;

        private Viewport viewport;

        //chased object properties (set externally each frame)
        private Vector3 chasePosition;
        private Vector3 chaseDirection;
        private Vector3 up = Vector3.Up;

        //desired camera positioning (set when creating camera or changing view)
        private Vector3 desiredPositionOffset;
        private Vector3 desiredPosition;
        private Vector3 lookAtOffset;
        private Vector3 lookAt;

        //camera physics (typically set when creating camera)
        private float stiffness = 2800.0f;
        private float damping = 600.0f;
        private float mass = 50.0f;

        //current camera properties (updated by camera physics)
        private Vector3 position;
        private Vector3 velocity;

        //perspective properties
        private float aspectRatio;
        private float fieldOfView = MathHelper.ToRadians(45.0f);
        private float nearPlaneDistance;
        private float farPlaneDistance;

        //matrix properties
        private Matrix view;
        private Matrix projection;

        public Vector3 OriginalPositionOffset;
        public Vector3 CameraDirection;
        public Vector3 OriginalPosition;

        public float LookAtCameraDistance;
        public float PlayerCameraDistance;

        public bool collisionDetected = false;

        public Camera(Viewport viewport, Vector3 desiredPositionOffset, Vector3 lookAtOffset, float nearPlaneDistance, float farPlaneDistance, Player player)
        {
            this.viewport = viewport;
            this.desiredPositionOffset = desiredPositionOffset;
            this.lookAtOffset = lookAtOffset;
            this.nearPlaneDistance = nearPlaneDistance;
            this.farPlaneDistance = farPlaneDistance;
            this.player = player;
            this.aspectRatio = (float)viewport.Width / viewport.Height;

            this.OriginalPositionOffset = desiredPositionOffset;

            UpdateCameraChaseTarget();
            Reset();
        }

        #endregion

        #region methods

        /// <summary>
        /// Update the values to be chased by the camera
        /// </summary>
        public void UpdateCameraChaseTarget()
        {
            chasePosition = player.Position;
            chaseDirection = player.Direction;
            up = Vector3.Up;
        }

        /// <summary>
        /// Rebuilds object space values in world space. Invoke before publicly
        /// returning or privately accessing world space values.
        /// </summary>
        private void UpdateWorldPositions()
        {
            // Construct a matrix to transform from object space to worldspace
            Matrix transform = Matrix.Identity;
            transform.Forward = ChaseDirection;
            transform.Up = Up;
            transform.Right = Vector3.Cross(Up, ChaseDirection);

            // Calculate desired camera properties in world space
            desiredPosition = ChasePosition +
                Vector3.TransformNormal(DesiredPositionOffset, transform);
            lookAt = ChasePosition +
                Vector3.TransformNormal(LookAtOffset, transform);

            OriginalPosition = ChasePosition +
                Vector3.TransformNormal( OriginalPositionOffset, transform );
        }

        /// <summary>
        /// Rebuilds camera's view and projection matricies.
        /// </summary>
        private void UpdateMatrices()
        {
            view = Matrix.CreateLookAt(this.Position, this.LookAt, this.Up);
            projection = Matrix.CreatePerspectiveFieldOfView(FieldOfView,
                AspectRatio, NearPlaneDistance, FarPlaneDistance);
        }

        /// <summary>
        /// Forces camera to be at desired position and to stop moving. The is useful
        /// when the chased object is first created or after it has been teleported.
        /// Failing to call this after a large change to the chased object's position
        /// will result in the camera quickly flying across the world.
        /// </summary>
        public void Reset()
        {
            UpdateWorldPositions();

            // Stop motion
            velocity = Vector3.Zero;

            // Force desired position
            position = desiredPosition;

            CameraDirection = ( lookAtOffset - desiredPositionOffset );
            LookAtCameraDistance = CameraDirection.Length();

            PlayerCameraDistance = ( position - chasePosition ).Length();

            UpdateMatrices();
        }

        /// <summary>
        /// Animates the camera from its current position towards the desired offset
        /// behind the chased object. The camera's animation is controlled by a simple
        /// physical spring attached to the camera and anchored to the desired position.
        /// </summary>
        public void Update(GameTime gameTime)
        {
            if (gameTime == null)
                throw new ArgumentNullException("gameTime");

            UpdateCameraChaseTarget();
            UpdateWorldPositions();

            float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

            // Calculate spring force
            Vector3 stretch = position - desiredPosition;
            Vector3 force = -stiffness * stretch - damping * velocity;

            // Apply acceleration
            Vector3 acceleration = force / mass;
            velocity += acceleration * elapsed;

            // Apply velocity
            position += velocity * elapsed;

            UpdateMatrices();
        }

        #endregion

        #region properties


        public Player Player
        {
            get { return player; }
            set { player = value; }
        }

        public Viewport Viewport
        {
            get
            {
                return viewport;
            }
            set
            {
                viewport = value;
            }
        }

        #region chased object properties (set externally each frame)

        /// <summary>
        /// Position of object being chased.
        /// </summary>
        public Vector3 ChasePosition
        {
            get
            {
                return chasePosition;
            }
            set
            {
                chasePosition = value;
            }
        }

        /// <summary>
        /// Direction the chased object is facing.
        /// </summary>
        public Vector3 ChaseDirection
        {
            get
            {
                return chaseDirection;
            }
            set
            {
                chaseDirection = value;
            }
        }

        /// <summary>
        /// Chased object's Up vector.
        /// </summary>
        public Vector3 Up
        {
            get
            {
                return up;
            }
            set
            {
                up = value;
            }
        }

        #endregion

        #region desired camera positioning (set when creating camera or changing view)

        /// <summary>
        /// Desired camera position in the chased object's coordinate system.
        /// </summary>
        public Vector3 DesiredPositionOffset
        {
            get
            {
                return desiredPositionOffset;
            }
            set
            {
                desiredPositionOffset = value;
            }
        }

        /// <summary>
        /// Desired camera position in world space.
        /// </summary>
        public Vector3 DesiredPosition
        {
            get
            {
                // Ensure correct value even if update has not been called this frame
                UpdateWorldPositions();

                return desiredPosition;
            }
        }

        /// <summary>
        /// Look at point in the chased object's coordinate system.
        /// </summary>
        public Vector3 LookAtOffset
        {
            get
            {
                return lookAtOffset;
            }
            set
            {
                lookAtOffset = value;
            }
        }

        /// <summary>
        /// Look at point in world space.
        /// </summary>
        public Vector3 LookAt
        {
            get
            {
                // Ensure correct value even if update has not been called this frame
                UpdateWorldPositions();

                return lookAt;
            }
        }

        #endregion

        #region camera physics (typically set when creating camera)

        /// <summary>
        /// Physics coefficient which controls the influence of the camera's position
        /// over the spring force. The stiffer the spring, the closer it will stay to
        /// the chased object.
        /// </summary>
        public float Stiffness
        {
            get
            {
                return stiffness;
            }
            set
            {
                stiffness = value;
            }
        }

        /// <summary>
        /// Physics coefficient which approximates internal friction of the spring.
        /// Sufficient damping will prevent the spring from oscillating infinitely.
        /// </summary>
        public float Damping
        {
            get
            {
                return damping;
            }
            set
            {
                damping = value;
            }
        }

        /// <summary>
        /// Mass of the camera body. Heaver objects require stiffer springs with less
        /// damping to move at the same rate as lighter objects.
        /// </summary>
        public float Mass
        {
            get
            {
                return mass;
            }
            set
            {
                mass = value;
            }
        }

        #endregion

        #region current camera properties (updated by camera physics)

        /// <summary>
        /// Position of camera in world space.
        /// </summary>
        public Vector3 Position
        {
            get
            {
                return position;
            }
        }

        /// <summary>
        /// Velocity of camera.
        /// </summary>
        public Vector3 Velocity
        {
            get
            {
                return velocity;
            }
        }

        #endregion

        #region perspective properties

        /// <summary>
        /// Perspective aspect ratio. Default value should be overriden by application.
        /// </summary>
        public float AspectRatio
        {
            get
            {
                return aspectRatio;
            }
            set
            {
                aspectRatio = value;
            }
        }

        /// <summary>
        /// Perspective field of view.
        /// </summary>
        public float FieldOfView
        {
            get
            {
                return fieldOfView;
            }
            set
            {
                fieldOfView = value;
            }
        }

        /// <summary>
        /// Distance to the near clipping plane.
        /// </summary>
        public float NearPlaneDistance
        {
            get
            {
                return nearPlaneDistance;
            }
            set
            {
                nearPlaneDistance = value;
            }
        }

        /// <summary>
        /// Distance to the far clipping plane.
        /// </summary>
        public float FarPlaneDistance
        {
            get
            {
                return farPlaneDistance;
            }
            set
            {
                farPlaneDistance = value;
            }
        }

        #endregion

        #region matrix properties

        /// <summary>
        /// View transform matrix.
        /// </summary>
        public Matrix View
        {
            get
            {
                return view;
            }
        }

        /// <summary>
        /// Projecton transform matrix.
        /// </summary>
        public Matrix Projection
        {
            get
            {
                return projection;
            }
        }

        #endregion

        #endregion
    }
}
